package de.dnb.basics.filtering;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import de.dnb.basics.applicationComponents.Pair;
import de.dnb.basics.applicationComponents.PrefixPredicate;
import de.dnb.basics.applicationComponents.RangeCheckUtils;

/**
 * Utility-Klasse zur Anwendung eines Filters auf eine Liste von Werten.
 * 
 * @author Michael Inden
 * 
 * Copyright 2011 by Michael Inden 
 */
public final class FilterUtils {

    private FilterUtils() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * Splittet eine Liste in Teillisten. Jedes Element, das predicate 
     * erfüllt führt eine neue Teilliste an. Alle 
     * Teillisten sind nicht leer.
     * 
     * @param list		Liste, nicht null.
     * @param predicate	Prädikat
     * @return			Liste von Listen.
     */
    public static <E> List<List<E>> split(
        final List<E> list,
        final IPredicate<? super E> predicate) {
        List<List<E>> lists = new LinkedList<List<E>>();
        List<E> tempList = new LinkedList<E>();
        for (E e : list) {
            if (predicate.accept(e)) {
                // == 0 nur beim ersten Element
                if (tempList.size() != 0) {
                    lists.add(tempList);
                    tempList = new LinkedList<E>();
                }
            }
            tempList.add(e);
        }
        lists.add(tempList);
        return lists;

    }

    /**
     * Liefert eine gefilterte neue Liste. Es werden nur die Elemente
     * berücksichtigt, die filter erfüllen.
     * 
     * @param source	nicht null.
     * @param filter	nicht null.
     * @return			Neue Liste, nicht null.
     */
    public static <R, T extends R, S extends T> List<T> newFilteredList(
        final Iterable<S> source,
        final IPredicate<R> filter) {
        RangeCheckUtils.assertReferenceParamNotNull("source", source);
        RangeCheckUtils.assertReferenceParamNotNull("filter", filter);
        final List<T> filteredValues = new ArrayList<T>();
        for (final S current : source) {
            if (filter.accept(current))
                filteredValues.add(current);
        }
        return filteredValues;
    }

    /**
     * Liefert zwei Listen: eine, die predicate erfüllt, und eine zweite,
     * die predicate nicht erfüllt.
     * 
     * @param source	Liste, nicht null.
     * @param predicate	nicht null.
     * @return			Paar zweier Listen.
     */
    public static <R, T extends R, S extends T> Pair<List<T>, List<T>> divide(
        final Iterable<S> source,
        final IPredicate<R> predicate) {
        RangeCheckUtils.assertReferenceParamNotNull("source", source);
        RangeCheckUtils.assertReferenceParamNotNull("filter", predicate);
        final List<T> accepting = new ArrayList<T>();
        final List<T> denying = new ArrayList<T>();
        for (final S current : source) {
            if (predicate.accept(current))
                accepting.add(current);
            else
                denying.add(current);
        }
        return new Pair<List<T>, List<T>>(accepting, denying);
    }

    /**
     * Liefert zwei Listen: eine, deren Elemente in membersOfFirstList
     * enthalten sind und eine, für die das nicht gilt.
     * 
     * @param source                nicht null
     * @param membersOfFirstList    nicht null
     * @param <T>                   Typ
     * @return                      2 Listen, beide nicht null
     */
    @SafeVarargs
    public static <T> Pair<List<T>, List<T>> divide(
        final Iterable<T> source,
        final T... membersOfFirstList) {
        final Set<T> set = new HashSet<>(Arrays.asList(membersOfFirstList));
        IPredicate<T> predicate = new IPredicate<T>() {
            @Override
            public boolean accept(final T element) {
                return set.contains(element);
            }
        };
        return divide(source, predicate);

    }

    /**
     * Entfernt aus einer Collection Elemente, die nicht von filter
     * akzeptiert werden.
     * 
     * @param iterable	nicht null.
     * @param filter		nicht null.
     */
    public static <E> void filter(
        final Iterable<E> iterable,
        final IPredicate<? super E> filter) {
        RangeCheckUtils.assertReferenceParamNotNull("collection", iterable);
        RangeCheckUtils.assertReferenceParamNotNull("filter", filter);
        for (Iterator<E> it = iterable.iterator(); it.hasNext();)
            if (!filter.accept(it.next()))
                it.remove();
    }

    //@formatter:off
	/**
	 * Findet das erste Element einer iterierbaren Entität, das dem Kriterium 
	 * predicate entspricht.
	 * 
	 * @param iterable		nicht null.
	 * @param predicate		nicht null.
	 * @return				erstes gefundenes Element, sonst null.
	 */
	public static <E> E find(
			final Iterable<E> iterable, 
			final IPredicate<? super E> predicate) {
		RangeCheckUtils.assertReferenceParamNotNull(
				"collection", iterable);
		RangeCheckUtils.assertReferenceParamNotNull(
				"predicate", predicate);
		//@formatter:on
        for (E e : iterable) {
            if (predicate.accept(e))
                return e;
        }
        return null;
    }

    /**
     * Findet heraus, ob iterable ein Element enthält, aus das das Prädikat
     * zutrifft. 
     * @param iterable	nicht null.
     * @param predicate	nicht null.
     * 
     * @return 			true, wenn ein Element gefunden.
     */
    public static <E> boolean contains(
        final Iterable<E> iterable,
        final IPredicate<? super E> predicate) {
        RangeCheckUtils.assertReferenceParamNotNull("collection", iterable);
        RangeCheckUtils.assertReferenceParamNotNull("predicate", predicate);
        return find(iterable, predicate) != null;
    }

    /**
     * Findet den Zeiger auf das erste Element einer iterierbaren Entität, 
     * das gleich element ist. Es kommt equals() zum Einsatz.
     * 
     * @param iterable		nicht null.
     * @param element		ein zu suchendes Element
     * @return				Zeiger auf das erste gefundenes Element, 
     * 						sonst null.
     */
    public static <E> E findPointerTo(final Iterable<E> iterable, E element) {
        IPredicate<E> equalsPredicate = new Equals<E>(element);
        return find(iterable, equalsPredicate);
    }

    /**
     * Findet das erste Element einer Liste, das identisch element ist.
     * Es wird also == verwendet.
     * 
     * @param list			nicht null.
     * @param element		nicht null.
     * @return				index des ersten gefundenes Elementes, sonst -1.
     */
    public static
        <E, F extends E>
        int
        indexOfIdentical(List<E> list, F element) {
        RangeCheckUtils.assertReferenceParamNotNull("list", list);
        int index = 0;
        for (E e : list) {
            if (e == element) {
                return index;
            }
            index++;
        }
        return -1;
    }

    /**
     * Ersetzt das erste Listenelement, das identisch element ist,  
     * durch replacement. (Es wird == verwendet).
     * 
     * @param list
     * @param element
     * @param replacement
     * 
     * @return true, wenn etwas ersetzt werden konnte.
     */
    public static <E, F extends E, G extends E> boolean replaceIdentical(
        List<E> list,
        final F element,
        final G replacement) {
        RangeCheckUtils.assertReferenceParamNotNull("list", list);
        RangeCheckUtils.assertReferenceParamNotNull("element", element);
        RangeCheckUtils.assertReferenceParamNotNull("replacement", replacement);
        if (element == replacement)
            return false;
        int index = indexOfIdentical(list, element);
        if (index >= 0) {
            list.set(index, replacement);
            return true;
        }
        return false;
    }

    /**
     * Findet das erste Listenelement, das gleich element ist, und ersetzt
     * es durch replacement. Es wird equals() verwendet.
     * 
     * @param list
     * @param element
     * @param replacement
     * 
     * @return true, wenn etwas ersetzt wurde.
     */
    public static <E, F extends E, G extends E> boolean replace(
        List<E> list,
        final F element,
        final G replacement) {
        RangeCheckUtils.assertReferenceParamNotNull("list", list);
        RangeCheckUtils.assertReferenceParamNotNull("element", element);
        RangeCheckUtils.assertReferenceParamNotNull("replacement", replacement);
        if (element.equals(replacement))
            return false;
        int index = list.indexOf(element);
        if (index >= 0) {
            list.set(index, replacement);
            return true;
        }
        return false;
    }

    /**
     * Vergleicht 2 Listen, wobei nur die Elemente berücksichtigt werden,
     * die predicate erfüllen.
     * 
     * @param iterable1		nicht null
     * @param iterable2		nicht null
     * @param predicate		nicht null
     * @return				true, wenn nach Entfernen der nicht Erfüllenden
     * 						die Elemente der beiden Iterables Stück
     * 						für Stück gleich sind.
     */
    public static <E, F extends E, G extends E> boolean equals(
        Iterable<F> iterable1,
        Iterable<G> iterable2,
        IPredicate<E> predicate) {
        RangeCheckUtils.assertReferenceParamNotNull("iterable1", iterable1);
        RangeCheckUtils.assertReferenceParamNotNull("iterable2", iterable2);
        RangeCheckUtils.assertReferenceParamNotNull("predicate", predicate);
        List<E> list1 = newFilteredList(iterable1, predicate);
        List<E> list2 = newFilteredList(iterable2, predicate);
        return list1.equals(list2);
    }

    /**
     * Ersetzt ein altes Element der Menge durch ein neues an 
     * der Position des alten. Es wird auch ersetzt, wenn gilt
     * element.equals(replacement), da möglicherweise doch noch 
     * Unterschiede bestehen (Variable, die nicht in equals() eingehen,
     * aber weitere Information enthalten).
     * 
     * @param set			LinkedHashSet, nicht null.
     * @param element		nicht null
     * @param replacement	nicht null
     * @return				true, wenn ersetzt wurde.
     */
    public static <E, F extends E, G extends E> boolean replace(
        LinkedHashSet<E> set,
        F element,
        G replacement) {
        RangeCheckUtils.assertReferenceParamNotNull("set", set);
        RangeCheckUtils.assertReferenceParamNotNull("element", element);
        RangeCheckUtils.assertReferenceParamNotNull("replacement", replacement);
        // nicht nötig:
        if (element == replacement)
            return false;
        List<E> list = new LinkedList<E>(set);
        boolean replaced = replaceIdentical(list, element, replacement);
        if (replaced) {
            set.clear();
            set.addAll(list);
            return true;
        }
        return false;
    }

    /**
     * Erzeugt eine neue iterierbare Menge, mit function auf jedes Element
     * von from angewandt.
     * 
     * @param from		nicht null, kann leer sein.
     * @param function	nicht null.
     * @param <X>		domain.
     * @param <Y>		Range.
     * 
     * @return			Menge der Werte.
     */
    public static <X, Y> ArrayList<Y> map(
        final Iterable<X> from,
        final IFunction<? super X, ? extends Y> function) {
        RangeCheckUtils.assertReferenceParamNotNull("from", from);
        RangeCheckUtils.assertReferenceParamNotNull("function", function);
        ArrayList<Y> yVals = new ArrayList<Y>();
        for (X x : from) {
            Y y = function.apply(x);
            yVals.add(y);
        }
        return yVals;
    }

    /**
     * Erzeugt eine neue iterierbare Menge, mit function auf jedes Element
     * von from angewandt. Null-Werte werden gefiltert.
     * 
     * @param from		nicht null, kann leer sein.
     * @param function	nicht null.
     * @param <X>		domain.
     * @param <Y>		Range.
     * 
     * @return			Menge der Werte ohne null.
     */
    public static <X, Y> ArrayList<Y> mapNullFiltered(
        final Iterable<X> from,
        final IFunction<? super X, ? extends Y> function) {
        RangeCheckUtils.assertReferenceParamNotNull("from", from);
        RangeCheckUtils.assertReferenceParamNotNull("function", function);

        ArrayList<Y> yVals = map(from, function);
        filter(yVals, new Not<Y>(new NullPredicate<Y>()));
        return yVals;
    }

    /**
     * Realisiert die Listenoperation höherer Ordnung left fold. 
     * Auch bekannt als: reduce, accumulate, aggregate, compress, oder inject. 
     * Berechnet für eine Liste 
     * 		(a, b , c ...) 
     * den Wert
     * 		f...f(f(f(initial, a), b), c) ...) 
     * 
     * @param iterable	iterierbare Menge aus Werten, die mit dem
     * 					Definitionsbereich des zweiten Arguments von 
     * 					f verträglich sind (Untertypen von X).
     * @param function	Binäre Funktion, deren erstes argument mit 
     * 					dem Wertebereichverträglich ist und deren
     * 					zweites Argument ein Supertyp des Listentyps
     * 					ist..
     * @param initial	Anfangswert.
     * @param <X>		Definitionsbereich von f.
     * @param <F>		Wertebereich von f.
     * @return			gefalteten Wert.
     * 
     */
    public static <X, F> F foldLeft(
        final Iterable<? extends X> iterable,
        final IBinaryFunction<? extends F, F, X> function,
        final F initial) {
        F value = initial;
        for (X x : iterable) {
            value = function.apply(value, x);
        }
        return value;
    }

    public static void main(String... ar) {
        List<String> strings = Arrays.asList("a", "b", "c");
        System.out.println(divide(null, "a"));
    }
}
